<?php
namespace Core\MCache;

use RedisException, Core\HHO, Core\MCache;

/**
 * MCache 的 Redis 驱动。
 * 
 * - 注意：MCache\Redis 不完全兼容 MCache\APC。
 * 
 * @author SUSISOY
 */
final class Redis {
	/**
	 * 最近一个操作中出现的异常。
	 * @var int
	 */
	public $error = MCache::ERROR_NONE;
	/**
	 * 当前 MCache 驱动名称。
	 * @var string
	 */
	public $driver = MCache::DRIVER_REDIS;
	
	/**
	 * Redis 操作句柄。
	 * @var \Redis
	 */
	private $handle = NULL;
	/**
	 * MCSN 标识符。
	 * @var string
	 */
	private $mcsn = '';
	/**
	 * Redis 主机。
	 * @var string
	 */
	private $host = '';
	/**
	 * Redis 端口。
	 * @var int
	 */
	private $port = 0;
	/**
	 * 默认数据库。
	 * @var int
	 */
	private $db = 0;
	/**
	 * 选定的数据库。
	 * @var int
	 */
	private $use = 0;
	/**
	 * 所有键名自动前缀的内容。
	 * @var string
	 */
	private $prefix = '';

	public function __construct($mcsn) {
		$this->mcsn = $mcsn;
		
		$x = HHO::$config['mcsn'][$mcsn];
		
		isset($x['host']) && $this->host = $x['host'];
		isset($x['port']) && $this->port = $x['port'];
		isset($x['db']) && $this->db = $this->use = $x['db'];
		isset($x['prefix']) && $this->prefix = $x['prefix'];
	}

	/**
	 * 处理 RedisException 异常。
	 * 
	 * @param RedisException $e
	 * @return void
	 */
	private function _exception(RedisException $e) {
		switch ($e->getMessage()) {
			case 'Connection lost' :
				$this->error = $this->error ?  : MCache::ERROR_CONN_LOSE;
				$this->handle = NULL;
				
				break;
			default :
				$this->error = $this->error ?  : MCache::ERROR_UNKNOWN;
		}
		
		trigger_error("Redis handler error. (MCache: $this->mcsn, Db: $this->use, Message: " . $e->getMessage() . ')', E_USER_WARNING);
	}

	/**
	 * 初始化 Redis 连接。
	 * 
	 * @return boolean
	 */
	private function _init() {
		$this->error = MCache::ERROR_NONE;
		
		if ($this->handle) {
			return TRUE;
		}
		
		static $warning = TRUE;
		
		$Redis = new \Redis();
		/**
		 * Redis::pconnect() 第四个参数指定一个连接 ID，Redis::pconnect() 为同一个 ID 初始化 10 个持久连接。而当并发较高，10 个不够用时，Redis::pconnect() 将为该 ID 创建更多持久连接；
		 * Redis::pconnect() 同样为所有未指定连接 ID 的连接请求初始化 10 个持久连接，且不影响其它指定了连接 ID 的连接请求。而当并发较高，10 个不够用时，Redis::pconnect() 也同样为其创建更多持久连接；
		 * 注意：经测试，在同一个 PHP 进程内，同一个连接 ID，相同的主机连接参数，使用 Redis::pconnect() 创建的多个连接实为同一个持久连接，通过这些连接发启的事务将相互干扰；
		 */
		if (!$Redis->pconnect($this->host ?  : '127.0.0.1', $this->port ?  : 6379, 5, $this->mcsn)) {
			$warning || trigger_error("Unable to connect to the redis. (MCache: $this->mcsn)", E_USER_WARNING);
			$warning = FALSE;
			
			$this->error = MCache::ERROR_INIT_FAIL;
			
			return FALSE;
		}
		
		try {
			$this->db && $Redis->select($this->db);
			$this->prefix && $Redis->setOption(\Redis::OPT_PREFIX, $this->prefix);
		} catch (RedisException $e) {
			$this->error = MCache::ERROR_INIT_FAIL;
			
			return FALSE;
		}
		
		$warning = TRUE;
		
		$this->handle = $Redis;
		
		return TRUE;
	}

	/**
	 * 新建一个不存在的键值及其生存时间，实际动作为“SET if Not Exists”。
	 * - $ttl 为键值的生存秒数；
	 * - 成功，返回 TRUE；
	 * - 已存在或失败，返回 FALSE；
	 * - 错误：MCache::ERROR_REJECT；
	 * 
	 * @param string $key
	 * @param mixed $value
	 * @param int $ttl
	 * @return boolean
	 */
	public function add($key, $value, $ttl) {
		if ($ttl <= 0) {
			trigger_error('Invalid TTL', E_USER_WARNING);
		} elseif ($this->_init()) {
			$value = MCache::serialize($value);
			
			try {
				if ($this->handle->watch($key) && !$this->handle->exists($key)) {
					$x = $this->handle->multi()->setex($key, $ttl, $value)->exec();
					if ($x === FALSE) {
						$this->error = MCache::ERROR_REJECT;
						
						return FALSE;
					}
				}
				
				return isset($x[0]) && $x[0] ? TRUE : FALSE;
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return FALSE;
	}

	/**
	 * 新建一个不存在的且永不过期的键值，实际动作为“SET if Not Exists”。
	 * - 成功，返回 TRUE；
	 * - 已存在或失败，返回 FALSE；
	 * 
	 * @param string $key
	 * @param mixed $value
	 * @return boolean
	 */
	public function addNt($key, $value) {
		if ($this->_init()) {
			$value = MCache::serialize($value);
			
			try {
				return $this->handle->setnx($key, $value);
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return FALSE;
	}

	/**
	 * 设置一个键值及其生存时间，若键值不存在则创建之。
	 * - $ttl 为键值的生存秒数；
	 * - 成功，返回 TRUE；
	 * - 失败，返回 FALSE；
	 * 
	 * @param string $key
	 * @param mixed $value
	 * @param int $ttl
	 * @return boolean
	 */
	public function set($key, $value, $ttl) {
		if ($ttl <= 0) {
			trigger_error('Invalid TTL', E_USER_WARNING);
		} elseif ($this->_init()) {
			$value = MCache::serialize($value);
			
			try {
				return $this->handle->setex($key, $ttl, $value);
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return FALSE;
	}

	/**
	 * 设置一个永不过期的键值，若键值不存在则创建之。
	 * - 成功，返回 TRUE；
	 * - 失败，返回 FALSE；
	 *
	 * @param string $key
	 * @param mixed $value
	 * @return boolean
	 */
	public function setNt($key, $value) {
		if ($this->_init()) {
			$value = MCache::serialize($value);
			
			try {
				return $this->handle->set($key, $value);
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return FALSE;
	}

	/**
	 * 设置多个键值及其生存时间，若键值不存在则创建之。
	 * - $ttl 为键值的生存秒数；
	 * - 返回一个数组，即设置成功的键列表；
	 * 
	 * @param array $data
	 * @param int $ttl
	 * @return array
	 */
	public function mSet($data, $ttl) {
		$result = [];
		
		if ($ttl <= 0) {
			trigger_error('Invalid TTL', E_USER_WARNING);
		} elseif ($this->_init()) {
			try {
				$expire = time() + $ttl;
				
				foreach ($data as $k => &$v) {
					$this->handle->setex($k, $expire - time(), MCache::serialize($v)) && $result[] = $k;
				}
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return $result;
	}

	/**
	 * 设置多个永不过期的键值，若键值不存在则创建之。
	 * - 返回一个数组，即设置成功的键列表；
	 *
	 * @param array $data
	 * @return array
	 */
	public function mSetNt($data) {
		$result = [];
		
		if ($this->_init()) {
			try {
				foreach ($data as $k => &$v) {
					$v = MCache::serialize($v);
				}
				
				$this->handle->mset($data) && $result = array_keys($data);
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return $result;
	}

	/**
	 * 获得并更新一个键值及其生存时间，若键值不存在则新建之。
	 * - $ttl 为键值的生存秒数，默认为 0，永不过期；
	 * - 成功，返回原键值；
	 * - 失败或键值不存在，返回 NULL；
	 * 
	 * @param string $key
	 * @param mixed $value
	 * @param int $ttl
	 * @return mixed|NULL
	 */
	public function getSet($key, $value, $ttl = 0) {
		if ($this->_init()) {
			$value = MCache::serialize($value);
			
			try {
				if ($ttl > 0) {
					$x = $this->handle->multi()->getset($key, $value)->expire($key, $ttl)->exec();
					
					return isset($x[1]) && $x[1] && $x[0] !== FALSE ? MCache::unserialize($x[0]) : NULL;
				} else {
					$x = $this->handle->getset($key, $value);
					
					return $x !== FALSE ? MCache::unserialize($x) : NULL;
				}
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
	}

	/**
	 * 条件更新一个键值。
	 * - 成功，返回 TRUE；
	 * - 失败或键值不存在，返回 FALSE；
	 * - 错误：MCache::ERROR_REJECT；
	 * - 提示：$old 和 $new 允许是任意类型的数据，包括数组；
	 * 
	 * @param string $key
	 * @param mixed $old
	 * @param mixed $new
	 * @return boolean
	 */
	public function cas($key, $old, $new) {
		if ($this->_init()) {
			try {
				$this->handle->watch($key);
				
				$x = $this->handle->get($key);
				if ($x !== FALSE && MCache::unserialize($x) == $old) {
					$z = $this->handle->multi()->set($key, MCache::serialize($new))->exec();
					
					if ($z === FALSE) {
						$this->error = MCache::ERROR_REJECT;
						
						return FALSE;
					}
					
					return isset($z[0]) && $z[0];
				}
				
				$this->handle->unwatch();
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return FALSE;
	}

	/**
	 * 获得指定键值。
	 * - 键值不存在，返回 NULL；
	 * 
	 * @param string $key
	 * @return mixed|NULL
	 */
	public function get($key) {
		if ($this->_init()) {
			try {
				$x = $this->handle->get($key);
				
				return $x !== FALSE ? MCache::unserialize($x) : NULL;
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
	}

	/**
	 * 获得多个指定键值。
	 * - 键值不存在，返回数组中将不包含该键；
	 * 
	 * @param array $keys
	 * @return array
	 */
	public function mGet($keys) {
		$result = [];
		
		if ($this->_init()) {
			try {
				foreach ($this->handle->mGet($keys) as $k => $v) {
					$v !== FALSE && $result[$keys[$k]] = MCache::unserialize($v);
				}
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return $result;
	}

	/**
	 * 获得指定键值的过期时间。
	 * - 失败或已过期或不存在，返回负整数；
	 * - 成功，返回剩余秒数；
	 * 
	 * @param string $key
	 * @return int
	 */
	public function ttl($key) {
		if ($this->_init()) {
			try {
				return $this->handle->ttl($key);
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return 0 - PHP_INT_MAX;
	}

	/**
	 * 修改一个键值的生存时间（秒）。
	 * - 成功，返回 TRUE；
	 * - 失败或键值不存在，返回 FALSE；
	 * 
	 * @param string $key
	 * @param int $ttl
	 * @return boolean
	 */
	public function expire($key, $ttl) {
		if ($this->_init()) {
			try {
				return $this->handle->expire($key, $ttl);
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return FALSE;
	}

	/**
	 * 原子递增指定键值，若键值不存在则新建之，并初始值为 0。
	 * - $step 为整数，浮点数将被取整（非四舍五入）；
	 * - 成功，返回递增后的值；
	 * - 失败，返回 FALSE；
	 * 
	 * @param string $key
	 * @param int $step
	 * @return int|FALSE
	 */
	public function inc($key, $step = 1) {
		if ($this->_init()) {
			try {
				return $this->handle->incrBy($key, $step);
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return FALSE;
	}

	/**
	 * 原子递减指定键值，若键值不存在则新建之，并初始值为 0。
	 * - $step 为整数，浮点数将被取整（非四舍五入）；
	 * - 成功，返回递减后的值；
	 * - 失败，返回 FALSE；
	 * 
	 * @param string $key
	 * @param int $step
	 * @return int|FALSE
	 */
	public function dec($key, $step = 1) {
		if ($this->_init()) {
			try {
				return $this->handle->decrBy($key, $step);
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return FALSE;
	}

	/**
	 * 删除指定键值。
	 * - 成功，返回 TRUE；
	 * - 键值不存在或失败，返回 FALSE。
	 * 
	 * @param string $key
	 * @return boolean
	 */
	public function del($key) {
		if ($this->_init()) {
			if (is_scalar($key)) {
				try {
					return $this->handle->delete($key) === 1;
				} catch (RedisException $e) {
					$this->_exception($e);
				}
			} else {
				trigger_error(__CLASS__ . '::del() expects parameter 1 to be scalar', E_USER_WARNING);
			}
		}
		
		return FALSE;
	}

	/**
	 * 删除多个指定键值。
	 * - 成功，返回删除的数量，不存在的键值不计入其中，所以有可能为0。
	 * - 失败，返回0。
	 * 
	 * @param array $keys
	 * @return int
	 */
	public function mDel($keys) {
		if ($this->_init()) {
			if (is_array($keys)) {
				try {
					return call_user_func_array([$this->handle, 'delete'], $keys);
				} catch (RedisException $e) {
					$this->_exception($e);
				}
			} else {
				trigger_error(__CLASS__ . '::mDel() expects parameter 1 to be array', E_USER_WARNING);
			}
		}
		
		return 0;
	}

	/**
	 * 检测指定键值是否存在。
	 * - 存在，返回 TRUE；
	 * - 不存在，返回 FALSE；
	 * 
	 * @param string $key
	 * @return boolean
	 */
	public function exists($key) {
		if ($this->_init()) {
			try {
				return $this->handle->exists($key);
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return FALSE;
	}

	/**
	 * 将指定键名重命名为新键名，如果新键名已存在，则覆盖之。
	 * - 成功，返回 TRUE；
	 * - 失败或 $srckey 和 $dstkey 相同或 $srckey 不存在，返回 FALSE；
	 * 
	 * @param string $srckey
	 * @param string $dstkey
	 * @return boolean
	 */
	public function rename($srckey, $dstkey) {
		if ($this->_init()) {
			try {
				return $this->handle->rename($srckey, $dstkey);
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return FALSE;
	}

	/**
	 * 将指定键名重命名为新键名，如果新键名不存在的话。
	 * - 成功，返回 TRUE；
	 * - 失败或 $srckey 和 $dstkey 相同或 $srckey 不存在或 $dstkey 已存在，返回 FALSE；
	 *
	 * @param string $srckey
	 * @param string $dstkey
	 * @return boolean
	 */
	public function renameNx($srckey, $dstkey) {
		if ($this->_init()) {
			try {
				return $this->handle->renameNx($srckey, $dstkey);
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return FALSE;
	}

	/**
	 * 获得当前数据库中的当前前缀下的所有键。
	 * 
	 * @return array
	 */
	public function keys() {
		$keys = [];
		
		if ($this->_init()) {
			try {
				$l = strlen($this->prefix);
				
				foreach ($this->handle->keys('*') as $item) {
					$keys[] = substr($item, $l);
				}
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return $keys;
	}

	/**
	 * 切换数据库。
	 * 
	 * @param int $db
	 * @return boolean
	 */
	public function select($db = NULL) {
		if ($this->_init()) {
			is_null($db) && $db = $this->db;
			
			try {
				if ($this->handle->select($db)) {
					$this->use = $db;
					
					return TRUE;
				}
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return FALSE;
	}

	/**
	 * 清空当前数据库中的所有键值。
	 *
	 * @return boolean
	 */
	public function clearDb() {
		if ($this->_init()) {
			try {
				return $this->handle->flushDB();
			} catch (RedisException $e) {
				$this->_exception($e);
			}
		}
		
		return FALSE;
	}

	/**
	 * 获得当前 Redis 句柄。
	 * - 注意：通过 MCache\Redis 和 Redis 句柄两者所管理的数据互不兼容，比如 MCache\Redis 会在非标量数据被序列化后，前缀一个标识串；
	 * 
	 * @return \Redis|FALSE
	 */
	public function redis() {
		return $this->_init() ? $this->handle : FALSE;
	}

	public function __call($method, $args) {
		trigger_error("MCache drive 'Redis' does not exist '" . $method . "' operation", E_USER_ERROR);
	}
}